import { AppError, audit, transition } from "@autoscholar/core"; import { publicationDecisions, researchRuns } from "@autoscholar/db"; import { eq } from "drizzle-orm"; import { z } from "zod"; import { handler, json, parseBody } from "@/lib/server/api"; import { db } from "@/lib/server/db"; import { requireAdmin } from "@/lib/server/session"; export const runtime = "nodejs"; /** * Authorized override of a *soft* outcome only: moves quality_failed → ready_to_publish → awaiting_approval when every * failed gate is in the overridable set. Hard scientific blockers (fabrication, unverified citations, missing PDF, * synthetic/simulated artifacts, secrets) can never be overridden. */ const NON_OVERRIDABLE = new Set([ "worker.real", "references.verified", "pdf.valid", "secrets.none", "attempt.final", "checksums.valid", "results.consistency", "data.provenance", "claims.traced", "contract.artifacts", "disclosure.present", ]); export const POST = handler(async (req, { params }) => { const sess = await requireAdmin(req, { mutation: true }); const { runId } = (await params) as { runId: string }; const body = await parseBody(req, z.object({ rationale: z.string().min(20).max(2000) })); const d = db(); const [run] = await d.select().from(researchRuns).where(eq(researchRuns.id, runId)).limit(1); if (!run) throw new AppError("not_found", "run not found", 404); if (run.status !== "quality_failed") throw new AppError("conflict", `run is ${run.status}`, 409); const { qualityGateResults } = await import("@autoscholar/db"); const { desc } = await import("drizzle-orm"); const gates = await d .select() .from(qualityGateResults) .where(eq(qualityGateResults.runId, runId)) .orderBy(desc(qualityGateResults.createdAt)); const latest = gates.filter((g) => g.evaluationId === gates[0]?.evaluationId); const failedHard = latest.filter((g) => g.severity === "hard" && g.status === "fail"); const blocked = failedHard.filter((g) => NON_OVERRIDABLE.has(g.gate)); if (blocked.length) throw new AppError( "forbidden", `non-overridable gates failed: ${blocked.map((g) => g.gate).join(", ")}`, 403, ); await d.transaction(async (tx) => { await tx.insert(publicationDecisions).values({ runId, evaluationId: latest[0]?.evaluationId ?? null, decision: "hold", decidedBy: `admin:${sess.userId}`, rationale: body.rationale, override: true, overrideRationale: body.rationale, gateSummary: { overriddenGates: failedHard.map((g) => g.gate) }, }); await transition(tx, runId, "ready_to_publish", { actor: `admin:${sess.userId}`, reason: `override: ${body.rationale}`, data: { overriddenGates: failedHard.map((g) => g.gate) }, }); await transition(tx, runId, "awaiting_approval", { actor: `admin:${sess.userId}`, reason: "override requires explicit publish action", }); await audit( tx, { type: "admin", id: sess.userId, ip: sess.ip }, "quality.override", { type: "run", id: runId }, { rationale: body.rationale, gates: failedHard.map((g) => g.gate) }, ); }); return json({ ok: true, status: "awaiting_approval" }); });